package com.meanbean.studytime;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.meanbean.studytime.controls.Button;

import javafx.scene.image.Image;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;

/**
 * Represents a Study Time File (stf).
 * @author Derek Webb
 */
public class StudyTimeFile
{
	private Path stfFilePath = null;
	private Path imageFilePath = null;
	private Path dataFilePath = null;
	
	private LinkedList<Button> studyTimeButtonsList = new LinkedList<Button>();
	
	private boolean changesMade = false;
	
	
	/**
	 * A static function used to create StudyTimeFile objects with a given STF.
	 * @param studyTimeFile The STF to initialize from.
	 * @return A StudyTimeFile object.
	 * @throws Exception
	 */
	public static StudyTimeFile createWithStudyTimeFile(Path studyTimeFile) throws Exception
	{
		StudyTimeFile s = new StudyTimeFile();
		s.initWithStudyTimeFile(studyTimeFile);
		
		return s;
	}
	
	/**
	 * A static functions used to create StudyTimeFile objects with a given image file.
	 * @param imageFile The image file to initialize with.
	 * @return A StudyTimeFile object.
	 * @throws Exception
	 */
	public static StudyTimeFile createWithImageFile(Path imageFile) throws Exception
	{
		StudyTimeFile s = new StudyTimeFile();
		s.initWithImageFile(imageFile);
		
		return s;
	}
	
	/**
	 * Creates a StudyTimeFile object.
	 */
	private StudyTimeFile()
	{
	}
	
	/**
	 * Initializes a StudyTimeFile object with the given STF file.
	 * @param studyTimeFile The STF file to open and use in this object.
	 * @throws IOException 
	 */
	private void initWithStudyTimeFile(Path studyTimeFile) throws Exception
	{
		// Store the studyTimeFile
		stfFilePath = studyTimeFile;
		
		// Creates the temp files
		createTempFiles();
		
		// Read and store the files from the STF.
		readSTF(studyTimeFile);
		
		// Initialize the StudyTimeButtons
		initStudyTimeButtonList();
	}
	
	/**
	 * Initializes a StudyTimeFile object with a given image file.
	 * This will create a temp copy of the image file and uses this temp copy as the image for this object.
	 * @param imageFile The image file to be stored in the StudyTimeFile.
	 */
	private void initWithImageFile(Path imageFile) throws Exception
	{
		// Create a temp directory
		Path tempDirectory = Files.createTempDirectory(null);
		
		// Create a temp file
		Path tempImageFile = Files.createTempFile(tempDirectory, null, "Image.jpg");
		
		// Create the OutputStream
		OutputStream out = new FileOutputStream(tempImageFile.toString());
		
		// Copy the imageFile to the temp directory
		Files.copy(imageFile, out);
		
		// Store the temp image file
		imageFilePath = tempImageFile;
		
		// Mark that changes have been made and a save is necessary
		changesMade = true;
	}
	
	/**
	 * Creates the temporary directory and assorted files.
	 * @throws Exception
	 */
	private void createTempFiles() throws Exception
	{
		// Create a temp directory
		Path tempDirectory = Files.createTempDirectory(null);
		
		// Create and store a temp image file
		imageFilePath = Files.createTempFile(tempDirectory, null, "Image.jpg");
		
		// Create and store a temp data file
		dataFilePath = Files.createTempFile(tempDirectory, null, "StudyTimeFileData.xml");
	}
	
	/**
	 * Extracts the contents of the STF file to a temp directory.
	 * @param studyTimeFile The STF file to open.
	 * @throws Exception
	 */
	private void readSTF(Path studyTimeFile) throws Exception
	{
		// Open the STF (which is a zip file)
		ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(studyTimeFile.toString()));
		
		// Create a ZipEntry reference
		ZipEntry zipEntry;
		
		// Read all of the entries in the zip file, one by one
		while ((zipEntry = zipInputStream.getNextEntry()) != null)
		{
			// Get the path to the file where we will store the data in the ZipEntry
			Path path = getPathToStoreZipEntryFile(zipEntry);
			
			// Write the data from the ZipInputStream to the path
			writeZipInputStreamToPath(zipInputStream, path);
		}
		
		// Close the ZipInputStream
		zipInputStream.close();
	}
	
	/**
	 * Determines which file is being read from the ZipInputStream, based on the file name, and then returns the Path.
	 * @param zipEntry The ZipEntry you want to link to the "Image.jpg" or the "StudyTimeFileData.xml" path.
	 * @return The path to either the "Image.jpg" or the "StudyTimeFileData.xml" file, or null if it is none of those.
	 */
	private Path getPathToStoreZipEntryFile(ZipEntry zipEntry)
	{
		if (zipEntry.getName().endsWith("Image.jpg"))
			return imageFilePath;
		else if (zipEntry.getName().endsWith("StudyTimeFileData.xml"))
			return dataFilePath;
		else
			return null;
	}
	
	/**
	 * Writes the current ZipEntry from the ZipInputStream into the Path.
	 * @param zipInputStream The open ZipInputStream to read from
	 * @param path The Path to write to.
	 * @throws Exception
	 */
	private void writeZipInputStreamToPath(ZipInputStream zipInputStream, Path path) throws Exception
	{
		// Open the output stream
		OutputStream out = new FileOutputStream(path.toString());
		
		// Write all of the bytes from the ZipInputStream into the OutputStream for this ZipEntry
		byte[] buffer = new byte[1024];
		int length;
		while((length = zipInputStream.read(buffer)) > 0)
		{
			out.write(buffer, 0, length);
		}
		
		// Close the OutputStream
		out.close();
	}
	
	/**
	 * Reads the StudyTimeData.xml file and creates a StudyTimeButton for each entry.
	 * @throws Exception
	 */
	private void initStudyTimeButtonList() throws Exception
	{
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder documentBuilder = dbFactory.newDocumentBuilder();
		Document doc = documentBuilder.parse(dataFilePath.toString());
		
		doc.getDocumentElement().normalize();
		
		NodeList nodes = doc.getElementsByTagName("button");
		
		for (int i = 0; i < nodes.getLength(); ++i)
		{
			Node node = nodes.item(i);
			
			if (node.getNodeType() == Node.ELEMENT_NODE)
			{
				Element element = (Element) node;
				
				int xPosition = Integer.parseInt(getValue("x-position", element));
				int yPosition = Integer.parseInt(getValue("y-position", element));
				String answer = getValue("user-provided-answer", element);
				
				Button button = new Button(xPosition, yPosition, answer);
				
				studyTimeButtonsList.add(button);
			}
		}
	}
	
	/**
	 * Gets the value of a single node in XML.
	 * @param tag The tag to look for.
	 * @param element The current element to look in.
	 * @return The String value of an individual node.
	 */
	private static String getValue(String tag, Element element)
	{
		NodeList nodes = element.getElementsByTagName(tag).item(0).getChildNodes();
		Node node = (Node) nodes.item(0);
		return node.getNodeValue();
	}
	
	/**
	 * @return The image stored in the StudyTimeFile.
	 */
	public Image getImage()
	{
		return new Image("file:" + imageFilePath.toString());
	}
	
	/**
	 * @return The boolean value representing whether or not changes have been made and need to be saved.
	 */
	public boolean needsToSave()
	{
		return changesMade;
	}
	
	/**
	 * @param buttonList The list of buttons to store.
	 */
	public void setButtonList(LinkedList<Button> buttonList)
	{
		// Check for changes
		for (int i = 0; i < studyTimeButtonsList.size(); i++)
		{
			if (buttonList.get(i).getX() != studyTimeButtonsList.get(i).getX() ||
				buttonList.get(i).getY() != studyTimeButtonsList.get(i).getY() ||
				!buttonList.get(i).getAnswer().equals(studyTimeButtonsList.get(i).getAnswer()))
			{
				changesMade = true;
			}
		}
		
		this.studyTimeButtonsList = buttonList;
	}
	
	/**
	 * @return The LinkedList containing all of the StudyTimeButtons in this STF.
	 */
	public LinkedList<Button> getButtonList()
	{
		return studyTimeButtonsList;
	}
	
	public void save() throws Exception
	{
		if (stfFilePath != null)
		{
			writeToSTF();
		}
		else
			saveAs();
	}
	
	public void saveAs() throws Exception
	{
		FileChooser fileChooser = new FileChooser();
		fileChooser.setTitle("Save the Study Time File (STF)");
		fileChooser.getExtensionFilters().add(new ExtensionFilter("STF", "*.stf"));
		
		File file = fileChooser.showSaveDialog(StudyTimeMain.studyTimeStage);
		
		// If the user selected a file
		if (file != null)
		{
			// Verify that the extension is correct
			if (!file.getPath().endsWith(".stf"))
				file = new File(file.getPath()+ ".stf");
			
			// Save the file path
			stfFilePath = file.toPath();
			
			// Write the bytes
			writeToSTF();
		}
	}
	
	private void writeToSTF() throws Exception
	{
		// Save the current buttons on the StudyControl
		if (StudyTimeStage.getMode() == StudyTimeStage.Mode.Edit)
			StudyTimeStage.getStudyControl().saveButtons();
		
		// The zip file we are saving as an stf file
		ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(stfFilePath.toString()));
		
		// Save the image file
		writeZipEntry("Image.jpg", imageFilePath.toString(), zipOutputStream);
		
		// Save the data XML file
		createDataXML();
		writeZipEntry("StudyTimeFileData.xml", dataFilePath.toString(), zipOutputStream);
		
		// Close the ZipOutputStream
		zipOutputStream.close();
	}
	
	private void writeZipEntry(String fileName, String inputFilePath, ZipOutputStream zipOutputStream) throws Exception
	{
		// File to read from
		FileInputStream fileInputStream = new FileInputStream(inputFilePath);
		
		// Create the ZipEntry
		zipOutputStream.putNextEntry(new ZipEntry(fileName));
		
		// Write to the ZipEntry
		int length;
		byte[] buffer = new byte[1024];
		while ((length = fileInputStream.read(buffer)) > 0)
			zipOutputStream.write(buffer, 0, length);
		
		// Close the streams
		zipOutputStream.closeEntry();
		fileInputStream.close();
	}
	
	private String createDataXMLString()
	{
		String xmlString =
            "<study-time-file-data>\n" +
            "   <buttons>\n";

        for (Button button : studyTimeButtonsList)
        {
            xmlString +=
                "      <button>\n" +
                "         <x-position>" + button.getX() + "</x-position>\n" +
                "         <y-position>" + button.getY() + "</y-position>\n" +
                "         <user-provided-answer>" + button.getAnswer() + "</user-provided-answer>\n" +
                "      </button>\n";
        }

        xmlString +=
            "   </buttons>\n" +
            "</study-time-file-data>\n";

        return xmlString;
	}
	
	private void createDataXML() throws Exception
	{
		// Create new XML file
		Path tempDirectory = Files.createTempDirectory(null);
		dataFilePath = Files.createTempFile(tempDirectory, null, "StudyTimeFileData.xml");
		
		// Get the XML string
		String xmlString = createDataXMLString();
		
		// Write to the file
		BufferedWriter writer = Files.newBufferedWriter(dataFilePath, Charset.defaultCharset());
		writer.write(xmlString);
		writer.flush();
	}
}
